// Copyright 2012 Google Inc. All Rights Reserved.

package com.google.superlists.servlet;

import com.google.api.client.googleapis.batch.BatchRequest;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.services.calendar.Calendar;
import com.google.api.services.calendar.Calendar.Events;
import com.google.api.services.calendar.model.Event;
import com.google.api.services.calendar.model.EventDateTime;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.Drive.Files.Get;
import com.google.api.services.drive.Drive.Files.Insert;
import com.google.api.services.drive.Drive.Files.Update;
import com.google.api.services.drive.model.File;
import com.google.api.services.tasks.Tasks;
import com.google.api.services.tasks.Tasks.Tasklists;
import com.google.api.services.tasks.Tasks.TasksOperations;
import com.google.api.services.tasks.model.Task;
import com.google.api.services.tasks.model.TaskList;
import com.google.common.base.Charsets;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Futures;
import com.google.superlists.model.ActionResult;
import com.google.superlists.model.SuperList;
import com.google.superlists.model.SuperListItem;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Action handling saving of a SuperList.
 *
 * @author sven@google.com (Sven Mawson)
 */
public class SaveAction extends BaseAction {

  private final String data;

  public SaveAction(
      HttpServlet servlet, HttpServletRequest req, HttpServletResponse resp, String data) {
    super(servlet, req, resp);

    this.data = data;
  }

  @Override
  protected ActionResult handle() throws IOException {
    SuperList list = SuperList.fromJson(data);
    String savedId = saveList(list);

    ActionResult result = new ActionResult();
    result.id = savedId;
    result.list = list;
    return result;
  }

  /**
   * Saves a SuperList to the backing store (Drive).
   */
  protected String saveList(SuperList list) throws IOException {
    boolean isUpdate = list.id != null && !list.id.isEmpty();

    // Sync the list with other services. This will populate fields on the list
    // so we can not batch it along with saving the list itself.
    File file = syncList(isUpdate, list);
    file.setTitle(list.name);

    // Create a content blob.
    String contentJson = list.content.toJson();
    ByteArrayContent content =
        new ByteArrayContent("application/x-super-list", contentJson.getBytes(Charsets.UTF_8));
    addMetric("save.fileLength", contentJson.length());
    addMetric("request.size", contentJson.length());

    // Insert or update the content.
    // Note(sven): This doesn't do a patch because we can't patch multipart yet.
    Drive service = getDriveService();
    File result;
    String name = (isUpdate) ? "save.files.update()" : "save.files.insert()";
    start(name);
    if (isUpdate) {
      Update update = service.files().update(list.id, file, content);
      if (usePartialResponses()) {
        update.setFields("id");
      }
      update.getMediaHttpUploader().setDirectUploadEnabled(true);
      result = update.execute();
    } else {
      Insert insert = service.files().insert(file, content);
      if (usePartialResponses()) {
        insert.setFields("id");
      }
      insert.getMediaHttpUploader().setDirectUploadEnabled(true);
      result = insert.execute();
    }
    stop(name);
    addContentLength("save.files.responseLength", result.getResponseHeaders());
    addContentLength("response.size", result.getResponseHeaders());

    return result.getId();
  }

  /**
   * Syncs a superlist to other services. Currently handles both tasks and calendar syncing.
   */
  private File syncList(boolean isUpdate, SuperList list) throws IOException {

    // Remove any empty items just in case they snuck through the JS.
    list.removeEmptyItems();

    Tasks tasksService = getTasksService();
    BatchRequest batch = (useBatch()) ? tasksService.batch() : null;

    // If this is an insert, we create a new file, otherwise we have to retrieve
    // the existing file so we can update it.
    Future<File> fileFuture;
    if (isUpdate) {
      Get getFile = getDriveService().files().get(list.id);
      if (batch != null) {
        FutureCallback<File> callback = createCallback("save.files.getLength");
        getFile.queue(batch, callback);
        fileFuture = callback.getFuture();
      } else {
        start("save.files.get()");
        File file = getFile.execute();
        stop("save.files.get()");
        addContentLength("save.files.getLength", file.getResponseHeaders());
        addContentLength("response.size", file.getResponseHeaders());
        fileFuture = Futures.immediateFuture(file);
      }
    } else {
      fileFuture = Futures.immediateFuture(new File());
    }

    // Read the tasklist data, possibly as part of a batch.
    Future<TaskList> tasklistFuture = null;
    Future<com.google.api.services.tasks.model.Tasks> tasksFuture = null;
    String tasklistId = list.content.tasklistId;

    boolean hasTasklist = (tasklistId != null && !tasklistId.isEmpty());
    if (hasTasklist) {
      Tasklists.Get getRequest = tasksService.tasklists().get(tasklistId);
      if (usePartialResponses() && usePartialUpdate()) {
        getRequest.setFields("id,title");
      }

      TasksOperations.List listRequest = tasksService.tasks().list(tasklistId);
      if (usePartialResponses()) {
        if (usePartialUpdate()) {
          listRequest.setFields("items(id,title)");
        } else {
          listRequest.setFields("items");
        }
      }

      if (batch != null) {
        FutureCallback<TaskList> getCallback = createCallback("sync.tasklists.getLength");
        getRequest.queue(batch, getCallback);
        tasklistFuture = getCallback.getFuture();

        FutureCallback<com.google.api.services.tasks.model.Tasks> listCallback =
            createCallback("sync.tasks.listLength");
        listRequest.queue(batch, listCallback);
        tasksFuture = listCallback.getFuture();
      } else {
        start("sync.tasklists.get()");
        TaskList tasklist = getRequest.execute();
        addContentLength("sync.taskslists.getLength", tasklist.getResponseHeaders());
        addContentLength("response.size", tasklist.getResponseHeaders());
        tasklistFuture = Futures.immediateFuture(tasklist);
        stop("sync.tasklists.get()");

        start("sync.tasks.list()");
        com.google.api.services.tasks.model.Tasks tasks = listRequest.execute();
        addContentLength("sync.tasks.listLength", tasks.getResponseHeaders());
        addContentLength("response.size", tasks.getResponseHeaders());
        tasksFuture = Futures.immediateFuture(tasks);
        stop("sync.tasks.list()");
      }
    }

    // Read the calendar data, possibly as part of a batch.
    Calendar calendar = getCalendarService();
    List<Events.Get> eventRequests = createEvents(calendar, list);
    List<Future<Event>> eventsFuture = Lists.newArrayList();
    if (batch != null) {
      for (Events.Get eventRequest : eventRequests) {
        FutureCallback<Event> getCallback = createCallback("sync.events.getLength");
        eventRequest.queue(batch, getCallback);
        eventsFuture.add(getCallback.getFuture());
      }
    } else {
      String name = "sync.events.get(" + eventRequests.size() + ")";
      start(name);
      for (Events.Get eventRequest : eventRequests) {
        Event event = eventRequest.execute();
        eventsFuture.add(Futures.immediateFuture(event));
        addContentLength("sync.events.getLength", event.getResponseHeaders());
        addContentLength("response.size", event.getResponseHeaders());
      }
      stop(name);
    }

    if (batch != null) {
      int batchSize = batch.size();
      if (batchSize > 0) {
        String name = "sync.read.batch(" + batchSize + ")";
        start(name);
        batch.execute();
        stop(name);
      }
    }

    // Create new batch for the update portion of the sync (2 phases).
    batch = (useBatch()) ? calendar.batch() : null;

    Map<Integer, Future<Task>> insertedTasks = null;
    if (tasklistFuture != null) {
      insertedTasks = syncTasks(tasksService, batch, list, Futures.getUnchecked(tasklistFuture),
          Futures.getUnchecked(tasksFuture).getItems());
    }
    Map<Integer, Future<Event>> insertedEvents = syncEvents(calendar, batch, list, eventsFuture);

    if (batch != null) {
      int batchSize = batch.size();
      if (batchSize > 0) {
        String name = "sync.write.batch(" + batchSize + ")";
        start(name);
        batch.execute();
        stop(name);
      }
    }

    // Process the inserted data, propagating it back to the SuperList.
    if (insertedTasks != null) {
      processTaskResults(list, insertedTasks);
    }
    processEventResults(list, insertedEvents);

    // Return the File that we retrieved earlier.
    return Futures.getUnchecked(fileFuture);
  }

  /**
   * Returns a list of event requests.
   */
  private List<Events.Get> createEvents(Calendar calendar, SuperList list) throws IOException {
    // Collect all known events, including ones that may have been deleted.
    Set<String> knownIds = Sets.newHashSet();
    if (list.content.eventId != null) {
      knownIds.add(list.content.eventId);
    }
    for (SuperListItem item : list.content.items) {
      if (item.eventId != null) {
        knownIds.add(item.eventId);
      }
    }
    if (list.eventIds != null) {
      knownIds.addAll(list.eventIds);
    }

    // Create a Get event for each eventId.
    List<Events.Get> events = Lists.newArrayList();
    for (String eventId : knownIds) {
      Events.Get get = calendar.events().get("primary", eventId);
      if (usePartialResponses() && usePartialUpdate()) {
        // TODO(sven): Removing sequence results in an error when combined with
        // batch (works fine out of batch).  Investigate.
        get.setFields("id,summary,start,end,sequence");
      }
      events.add(get);
    }
    return events;
  }

  /**
   * Synchronizes the tasks with the list. Returns a map from item index to the future added task
   * for that item, so we can process adds after the batch has executed (if batching is enabled).
   */
  private Map<Integer, Future<Task>> syncTasks(
      Tasks taskService, BatchRequest batch, SuperList list, TaskList tasklist, List<Task> tasks)
      throws IOException {
    // Create a map from task id to task.
    Map<String, Task> taskMap = getTasks(tasks);
    List<Task> tasksToEdit = Lists.newArrayList();
    Map<Integer, Task> tasksToAdd = Maps.newLinkedHashMap();
    processTasks(list, taskMap, tasksToEdit, tasksToAdd);

    Tasklists.Patch patchTitle = null;
    Tasklists.Update updateTitle = null;
    List<TasksOperations.Update> updates = null;
    List<TasksOperations.Patch> patches = null;
    if (usePartialUpdate()) {
      patchTitle = createPatchTitle(taskService, list, tasklist);
      patches = createTaskPatches(taskService, tasklist, tasksToEdit);
    } else {
      updateTitle = createUpdateTitle(taskService, list, tasklist);
      updates = createTaskUpdates(taskService, tasklist, tasksToEdit);
    }
    Map<Integer, TasksOperations.Insert> inserts = createInserts(taskService, tasklist, tasksToAdd);
    List<TasksOperations.Delete> deletes = createDeletes(taskService, tasklist, taskMap.keySet());

    Map<Integer, Future<Task>> insertedTasks = Maps.newLinkedHashMap();
    if (batch != null) {
      if (updateTitle != null) {
        FutureCallback<TaskList> callback = createCallback("sync.tasklists.updateLength");
        updateTitle.queue(batch, callback);
      } else if (patchTitle != null) {
        FutureCallback<TaskList> callback = createCallback("sync.tasklists.patchLength");
        patchTitle.queue(batch, callback);
      }
      if (updates != null) {
        for (TasksOperations.Update update : updates) {
          FutureCallback<Task> callback = createCallback("sync.tasks.updateLength");
          update.queue(batch, callback);
        }
      } else if (patches != null) {
        for (TasksOperations.Patch patch : patches) {
          FutureCallback<Task> callback = createCallback("sync.tasks.patchLength");
          patch.queue(batch, callback);
        }
      }
      for (Map.Entry<Integer, TasksOperations.Insert> entry : inserts.entrySet()) {
        FutureCallback<Task> insertCallback = createCallback("sync.tasks.insertLength");
        entry.getValue().queue(batch, insertCallback);
        insertedTasks.put(entry.getKey(), insertCallback.getFuture());
      }
      for (TasksOperations.Delete delete : deletes) {
        FutureCallback<Void> callback = createCallback();
        delete.queue(batch, callback);
      }
    } else {
      if (updateTitle != null) {
        start("sync.tasklists.update()");
        TaskList updatedTitle = updateTitle.execute();
        addContentLength("sync.tasklists.updateLength", updatedTitle.getResponseHeaders());
        addContentLength("response.size", updatedTitle.getResponseHeaders());
        stop("sync.tasklists.update()");
      } else if (patchTitle != null) {
        start("sync.tasklists.patch()");
        TaskList patchedTitle = patchTitle.execute();
        addContentLength("sync.tasklists.patchLength", patchedTitle.getResponseHeaders());
        addContentLength("response.size", patchedTitle.getResponseHeaders());
        stop("sync.tasklists.patch()");
      }
      if (updates != null && !updates.isEmpty()) {
        String name = "sync.tasks.update(" + updates.size() + ")";
        start(name);
        for (TasksOperations.Update update : updates) {
          Task updatedTask = update.execute();
          addContentLength("sync.tasks.updateLength", updatedTask.getResponseHeaders());
          addContentLength("response.size", updatedTask.getResponseHeaders());
        }
        stop(name);
      } else if (patches != null && !patches.isEmpty()) {
        String name = "sync.tasks.patch(" + patches.size() + ")";
        start(name);
        for (TasksOperations.Patch patch : patches) {
          Task patchedTask = patch.execute();
          addContentLength("sync.tasks.patchLength", patchedTask.getResponseHeaders());
          addContentLength("response.size", patchedTask.getResponseHeaders());
        }
        stop(name);
      }
      if (!inserts.isEmpty()) {
        String name = "sync.tasks.insert(" + inserts.size() + ")";
        start(name);
        for (Map.Entry<Integer, TasksOperations.Insert> entry : inserts.entrySet()) {
          Task insertedTask = entry.getValue().execute();
          addContentLength("sync.tasks.insertLength", insertedTask.getResponseHeaders());
          addContentLength("response.size", insertedTask.getResponseHeaders());
          insertedTasks.put(entry.getKey(), Futures.immediateFuture(insertedTask));
        }
        stop(name);
      }
      if (!deletes.isEmpty()) {
        String name = "sync.tasks.delete(" + deletes.size() + ")";
        start(name);
        for (TasksOperations.Delete delete : deletes) {
          delete.execute();
        }
        stop(name);
      }
    }
    return insertedTasks;
  }

  /**
   * Returns a map from taskId to Task for the know tasks. Returns an empty map if we didn't
   * actually retrieve any tasks.
   */
  private Map<String, Task> getTasks(List<Task> tasks) {
    Map<String, Task> taskIdToTask = Maps.newLinkedHashMap();
    for (Task task : tasks) {
      taskIdToTask.put(task.getId(), task);
    }
    return taskIdToTask;
  }

  /**
   * Compares the current list with the task map to see if any changes are needed. Does nothing if
   * we have no tasks.
   */
  private void processTasks(SuperList list, Map<String, Task> taskMap, List<Task> tasksToEdit,
      Map<Integer, Task> tasksToAdd) {

    // Nothing to do if there is not a tasklist connected to this list.
    if (taskMap == null) {
      return;
    }

    // Go through our items, checking for new or edited items.
    for (int index = 0; index < list.content.items.size(); index++) {
      SuperListItem item = list.content.items.get(index);
      if (item.taskId != null && !item.taskId.isEmpty()) {
        Task task = taskMap.remove(item.taskId);
        if (task != null) {
          String taskTitle = task.getTitle();
          if (Objects.equal(taskTitle, item.name)) {
            continue;
          }
          task.setTitle(item.name);
          tasksToEdit.add(task);
          continue;
        }
      }
      // No id or id was deleted, add a new task.
      Task task = new Task();
      task.setTitle(item.name);
      tasksToAdd.put(index, task);
    }
  }

  /** Returns a patch action or null if patch is not needed. */
  private Tasklists.Patch createPatchTitle(Tasks tasks, SuperList list, TaskList tasklist) throws IOException {
    if (list.name.equals(tasklist.getTitle())) {
      return null;
    }
    tasklist.setTitle(list.name);
    Tasklists.Patch patchTitle = tasks.tasklists().patch(tasklist.getId(), tasklist);
    if (usePartialResponses()) {
      patchTitle.setFields("id");
    }
    return patchTitle;
  }

  /** Returns an update action or null if update is not needed. */
  private Tasklists.Update createUpdateTitle(Tasks tasks, SuperList list, TaskList tasklist)
      throws IOException {
    if (list.name.equals(tasklist.getTitle())) {
      return null;
    }
    tasklist.setTitle(list.name);
    Tasklists.Update updateTitle = tasks.tasklists().update(tasklist.getId(), tasklist);
    if (usePartialResponses()) {
      updateTitle.setFields("id");
    }
    return updateTitle;
  }

  /** Returns a list (possibly empty) or patch actions on the tasks. */
  private List<TasksOperations.Patch> createTaskPatches(
      Tasks tasks, TaskList tasklist, List<Task> tasksToEdit) throws IOException {
    List<TasksOperations.Patch> patches = Lists.newArrayList();
    for (Task task : tasksToEdit) {
      TasksOperations.Patch patch = tasks.tasks().patch(tasklist.getId(), task.getId(), task);
      if (usePartialResponses()) {
        patch.setFields("id");
      }
      patches.add(patch);
    }
    return patches;
  }

  /** Returns a list (possibly empty) of update actions on tasks. */
  private List<TasksOperations.Update> createTaskUpdates(
      Tasks tasks, TaskList tasklist, List<Task> tasksToEdit) throws IOException {
    List<TasksOperations.Update> updates = Lists.newArrayList();
    for (Task task : tasksToEdit) {
      TasksOperations.Update update = tasks.tasks().update(tasklist.getId(), task.getId(), task);
      if (usePartialResponses()) {
        update.setFields("id");
      }
      updates.add(update);
    }
    return updates;
  }

  /** Returns a map (possibly empty) from index to insert action for new tasks. */
  private Map<Integer, TasksOperations.Insert> createInserts(
      Tasks tasks, TaskList tasklist, Map<Integer, Task> tasksToAdd) throws IOException {
    Map<Integer, TasksOperations.Insert> inserts = Maps.newLinkedHashMap();
    for (Map.Entry<Integer, Task> entry : tasksToAdd.entrySet()) {
      TasksOperations.Insert insert = tasks.tasks().insert(tasklist.getId(), entry.getValue());
      if (usePartialResponses()) {
        insert.setFields("id");
      }
      inserts.put(entry.getKey(), insert);
    }
    return inserts;
  }

  /** Returns a list of delete operations to execute, possibly empty. */
  private List<TasksOperations.Delete> createDeletes(
      Tasks tasks, TaskList tasklist, Set<String> tasksToDelete) throws IOException {
    List<TasksOperations.Delete> deletes = Lists.newArrayList();
    for (String taskId : tasksToDelete) {
      // No partial on deletes.
      deletes.add(tasks.tasks().delete(tasklist.getId(), taskId));
    }
    return deletes;
  }

  /**
   * Process the map of inserted tasks back into the list.
   */
  private void processTaskResults(SuperList list, Map<Integer, Future<Task>> insertedTasks) {
    // Copy inserted task ids back to the superlist items.
    for (Map.Entry<Integer, Future<Task>> entry : insertedTasks.entrySet()) {
      Task task = Futures.getUnchecked(entry.getValue());
      list.content.items.get(entry.getKey()).taskId = task.getId();
    }
  }

  /**
   * Syncs the results from the tasks and calendar syncs back to the superlist. Returns a map from
   * item index to event created for that item.
   */
  private Map<Integer, Future<Event>> syncEvents(
      Calendar calendar, BatchRequest batch, SuperList list, List<Future<Event>> eventsFuture)
      throws IOException {

    // Process the events, which gathers the changes to make.
    Map<String, Event> eventMap = getEvents(eventsFuture);
    List<Event> eventsToEdit = Lists.newArrayList();
    Map<Integer, Event> eventsToAdd = Maps.newLinkedHashMap();
    processEvents(list, eventMap, eventsToEdit, eventsToAdd);

    // Create the updates, patches, inserts, and deletes for the events.
    List<Events.Patch> patches = null;
    List<Events.Update> updates = null;
    if (usePartialUpdate()) {
      patches = createPatches(calendar, eventsToEdit);
    } else {
      updates = createUpdates(calendar, eventsToEdit);
    }

    Map<Integer, Events.Insert> inserts = createInserts(calendar, eventsToAdd);
    List<Events.Delete> deletes = createDeletes(calendar, eventMap.keySet());

    // We only need the add results, the rest are ignored.
    Map<Integer, Future<Event>> insertedEvents = Maps.newLinkedHashMap();
    if (batch != null) {
      if (updates != null) {
        for (Events.Update update : updates) {
          FutureCallback<Event> callback = createCallback("sync.events.updatedLength");
          update.queue(batch, callback);
        }
      } else if (patches != null) {
        for (Events.Patch patch : patches) {
          FutureCallback<Event> callback = createCallback("sync.events.patchedLength");
          patch.queue(batch, callback);
        }
      }
      for (Map.Entry<Integer, Events.Insert> entry : inserts.entrySet()) {
        FutureCallback<Event> insertCallback = createCallback("sync.events.insertedLength");
        entry.getValue().queue(batch, insertCallback);
        insertedEvents.put(entry.getKey(), insertCallback.getFuture());
      }
      for (Events.Delete delete : deletes) {
        FutureCallback<Void> callback = createCallback();
        delete.queue(batch, callback);
      }
    } else {
      if (updates != null && !updates.isEmpty()) {
        String name = "sync.events.update(" + updates.size() + ")";
        start(name);
        for (Events.Update update : updates) {
          Event updated = update.execute();
          addContentLength("sync.events.updatedLength", updated.getResponseHeaders());
          addContentLength("response.size", updated.getResponseHeaders());
        }
        stop(name);
      } else if (patches != null && !patches.isEmpty()) {
        String name = "sync.events.patch(" + patches.size() + ")";
        start(name);
        for (Events.Patch patch : patches) {
          Event patched = patch.execute();
          addContentLength("sync.events.patchedLength", patched.getResponseHeaders());
          addContentLength("response.size", patched.getResponseHeaders());
        }
        stop(name);
      }
      if (!inserts.isEmpty()) {
        String name = "sync.events.insert(" + inserts.size() + ")";
        start(name);
        for (Map.Entry<Integer, Events.Insert> entry : inserts.entrySet()) {
          Event inserted = entry.getValue().execute();
          addContentLength("sync.events.insertedLength", inserted.getResponseHeaders());
          addContentLength("response.size", inserted.getResponseHeaders());
          insertedEvents.put(entry.getKey(), Futures.immediateFuture(inserted));
        }
        stop(name);
      }
      if (!deletes.isEmpty()) {
        String name = "sync.events.delete(" + deletes.size() + ")";
        start(name);
        for (Events.Delete delete : deletes) {
          delete.execute();
        }
        stop(name);
      }
    }
    return insertedEvents;
  }

  /** Process the event results back into the superlist. */
  private void processEventResults(SuperList list, Map<Integer, Future<Event>> insertedEvents) {
    for (Map.Entry<Integer, Future<Event>> entry : insertedEvents.entrySet()) {
      Event event = Futures.getUnchecked(entry.getValue());
      int index = entry.getKey();
      if (index == -1) {
        list.content.eventId = event.getId();
      } else {
        list.content.items.get(index).eventId = event.getId();
      }
    }
  }

  /** Creates a map from eventId to Event. */
  private Map<String, Event> getEvents(List<Future<Event>> eventsFuture) {
    Map<String, Event> eventIdToEvent = Maps.newLinkedHashMap();
    for (Future<Event> eventFuture : eventsFuture) {
      Event event = Futures.getUnchecked(eventFuture);
      eventIdToEvent.put(event.getId(), event);
    }
    return eventIdToEvent;
  }

  /**
   * Compares the current list and the results from calendar to see what changes need to be made.
   */
  private void processEvents(SuperList list, Map<String, Event> eventMap, List<Event> eventsToEdit,
      Map<Integer, Event> eventsToAdd) {
    String listDate = list.content.dueDate;
    Event event = null;
    if (list.content.eventId != null && !list.content.eventId.isEmpty()) {
      event = eventMap.remove(list.content.eventId);
    }
    if (event != null) {
      if (listDate == null || listDate.isEmpty()) {
        // Delete our existing event by adding it back to the map.
        eventMap.put(list.content.eventId, event);
        list.content.eventId = null;
      } else {
        // Compare event due date to item due date.
        if (!Objects.equal(listDate, event.getStart().getDate()) || !Objects.equal(
            list.name, event.getSummary())) {
          event.setSummary(list.name);
          EventDateTime itemDateTime = new EventDateTime().setDate(listDate);
          event.setStart(itemDateTime);
          event.setEnd(itemDateTime);
          eventsToEdit.add(event);
        }
      }
    } else if (listDate != null && !listDate.isEmpty()) {
      // No id or id was deleted, add a new event.
      event = new Event();
      event.setSummary(list.name);
      EventDateTime itemDateTime = new EventDateTime().setDate(listDate);
      event.setStart(itemDateTime);
      event.setEnd(itemDateTime);
      eventsToAdd.put(-1, event);
    }

    for (int index = 0; index < list.content.items.size(); index++) {
      SuperListItem item = list.content.items.get(index);
      String itemDate = item.dueDate;
      event = null;
      if (item.eventId != null && !item.eventId.isEmpty()) {
        event = eventMap.remove(item.eventId);
      }
      if (event != null) {
        if (itemDate == null || itemDate.isEmpty()) {
          // Delete our existing event by adding it back to the map.
          eventMap.put(item.eventId, event);
          item.eventId = null;
        } else {
          // Compare event due date to item due date.
          if (!Objects.equal(itemDate, event.getStart().getDate())
              || !Objects.equal(item.name, event.getSummary())) {
            event.setSummary(item.name);
            EventDateTime itemDateTime = new EventDateTime().setDate(itemDate);
            event.setStart(itemDateTime);
            event.setEnd(itemDateTime);
            eventsToEdit.add(event);
          }
        }
      } else if (itemDate != null && !itemDate.isEmpty()) {
        // No id or id was deleted, add a new event.
        event = new Event();
        event.setSummary(item.name);
        EventDateTime itemDateTime = new EventDateTime().setDate(itemDate);
        event.setStart(itemDateTime);
        event.setEnd(itemDateTime);
        eventsToAdd.put(index, event);
      }
    }
  }

  /** Returns a list of patch events for a calendar. */
  private List<Events.Patch> createPatches(Calendar calendar, List<Event> events)
      throws IOException {
    List<Events.Patch> patches = Lists.newArrayList();
    for (Event event : events) {
      Events.Patch patch = calendar.events().patch("primary", event.getId(), event);
      if (usePartialResponses()) {
        patch.setFields("id");
      }
      patches.add(patch);
    }
    return patches;
  }
  
  /** Returns a list of update events for a calendar. */
  private List<Events.Update> createUpdates(Calendar calendar, List<Event> events)
      throws IOException {
    List<Events.Update> updates = Lists.newArrayList();
    for (Event event : events) {
      Events.Update update = calendar.events().update("primary", event.getId(), event);
      if (usePartialResponses()) {
        update.setFields("id");
      }
      updates.add(update);
    }
    return updates;
  }

  /** Returns a map from item index to event to create for it. */
  private Map<Integer, Events.Insert> createInserts(Calendar calendar, Map<Integer, Event> events)
      throws IOException {
    Map<Integer, Events.Insert> inserts = Maps.newLinkedHashMap();
    for (Map.Entry<Integer, Event> entry : events.entrySet()) {
      Events.Insert insert = calendar.events().insert("primary", entry.getValue());
      if (usePartialResponses()) {
        insert.setFields("id");
      }
      inserts.put(entry.getKey(), insert);
    }
    return inserts;
  }

  /** Returns a list of delete events for the events to remove. */
  private List<Events.Delete> createDeletes(Calendar calendar, Set<String> eventIds)
      throws IOException {
    List<Events.Delete> deletes = Lists.newArrayList();
    for (String eventId : eventIds) {
      deletes.add(calendar.events().delete("primary", eventId));
    }
    return deletes;
  }
}
